﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace SharedGenomics.Domain
{
    [StructLayout(LayoutKind.Explicit)]
    public struct Scale : IEquatable<Scale>, IComparable<Scale>
    {
        #region Private Members

        [FieldOffset(0)]
        private long _longValue;

        [FieldOffset(0)]
        private double _doubleValue;

        [FieldOffset(8)]
        private bool _isLong;

        #endregion

        #region Constructors

        public Scale(long value)
        {
            this._doubleValue = 0.0;        //initialisation is required by the compiler but since it is unioned with _longValue, it's about to be junked...
            this._longValue = value;
            this._isLong = true;
        }

        public Scale(double value)
        {
            this._longValue = 0;            //initialisation is required by the compiler but since it is unioned with _doubleValue, it's about to be junked...
            this._doubleValue = value;
            this._isLong = false;
        }

        #endregion

        #region Public Methods

        public override bool Equals(object obj)
        {
            return (obj is Scale) ? this.Equals((Scale)obj) : false;
        }

        public override int GetHashCode()
        {
            return this.DoubleValue.GetHashCode();
        }

        public override string ToString()
        {
            return this._isLong ? this._longValue.ToString() : this._doubleValue.ToString();
        }

        public static bool TryParse(string s, out Scale value)
        {
            long l;
            double d;

            if (long.TryParse(s, out l))
            {
                value = new Scale(l);
                return true;
            }
            else if (double.TryParse(s, out d))
            {
                value = new Scale(d);
                return true;
            }
            else
            {
                value = new Scale();
                return false;
            }
        }

        public static Scale Parse(string s)
        {
            Scale value;
            if (TryParse(s, out value))
                return value;
            else
                throw new FormatException(String.Format("{0} is not a valid Scale value", s ?? "<null>"));
        }

        #endregion

        #region Implicit/Explicit Operators

        public static implicit operator Scale(long value)
        {
            return new Scale(value);
        }

        public static implicit operator Scale(double value)
        {
            return new Scale(value);
        }

        public static explicit operator long(Scale s)
        {
            return s.LongValue;
        }

        public static explicit operator int(Scale s)
        {
            return (int)s.LongValue;
        }

        public static explicit operator short(Scale s)
        {
            return (short)s.LongValue;
        }

        public static explicit operator byte(Scale s)
        {
            return (byte)s.LongValue;
        }

        public static explicit operator double(Scale s)
        {
            return s.DoubleValue;
        }

        public static explicit operator float(Scale s)
        {
            return (float)s.DoubleValue;
        }

        #endregion

        #region Equality Operators

        public static bool operator ==(Scale s1, Scale s2)
        {
            return s1.Equals(s2);
        }

        public static bool operator !=(Scale s1, Scale s2)
        {
            return !s1.Equals(s2);
        }

        public static bool operator >(Scale s1, Scale s2)
        {
            return s1.CompareTo(s2) > 0;
        }

        public static bool operator >=(Scale s1, Scale s2)
        {
            return s1.CompareTo(s2) >= 0;
        }

        public static bool operator <(Scale s1, Scale s2)
        {
            return s1.CompareTo(s2) < 0;
        }

        public static bool operator <=(Scale s1, Scale s2)
        {
            return s1.CompareTo(s2) <= 0;
        }

        #endregion

        #region Arithmetic Operators

        public static Scale operator +(Scale s1, Scale s2)
        {
            //if both s1 and s2 are long values, try to add the longs together
            if (s1._isLong && s2._isLong)
            {
                //check s1 + s2 is not an overflow for long
                double sum = (double)s1._longValue + (double)s2._longValue;
                return IsInRangeForLong(sum) ? new Scale(s1._longValue + s2._longValue) : new Scale(sum);
            }
            else
            {
                //add double values together
                return new Scale(s1.DoubleValue + s2.DoubleValue);
            }
        }

        public static Scale operator -(Scale s1, Scale s2)
        {
            if (s1._isLong && s2._isLong)
            {
                double val = ((double)s1._longValue - (double)s2._longValue);
                return IsInRangeForLong(val) ? new Scale(s1._longValue - s2._longValue) : new Scale(val);
            }
            else
            {
                return new Scale(s1.DoubleValue - s2.DoubleValue);
            }
        }

        public static Scale operator *(Scale s1, Scale s2)
        {
            if (s1._isLong && s2._isLong)
            {
                //check s1*s2 is not an overflow for long
                double mult = (double)s1._longValue * (double)s2._longValue;
                return IsInRangeForLong(mult) ? new Scale(s1._longValue * s2._longValue) : new Scale(mult);
            }
            else
            {
                return new Scale(s1.DoubleValue * s2.DoubleValue);
            }
        }

        public static Scale operator /(Scale s1, Scale s2)
        {
            if (s1._isLong && s2._isLong && s1._longValue % s2._longValue == 0)
            {
                long divValue = s1._longValue / s2._longValue;
                return new Scale(divValue);
            }
            else
            {
                return new Scale(s1.DoubleValue / s2.DoubleValue);
            }
        }

        #endregion

        #region Properties

        public long LongValue
        {
            get
            {
                if (this._isLong)
                    return this._longValue;
                else
                {
                    //check value is in range for long
                    double max = long.MaxValue - 513;
                    double min = long.MinValue + 513;

                    if (this._doubleValue >= min && this._doubleValue <= max)
                        return (long)this._doubleValue;
                    else
                        throw new OverflowException(String.Format("{0} is out of range for long", this._doubleValue));
                }
            }
        }

        public double DoubleValue
        {
            get
            {
                return this._isLong ? this._longValue : this._doubleValue;
            }
        }

        #endregion

        #region IEquatable<Scale> Members

        public bool Equals(Scale other)
        {
            return this.CompareTo(other) == 0;
        }

        #endregion

        #region IComparable<Scale> Members

        public int CompareTo(Scale other)
        {
            if (this._isLong || other._isLong)
            {
                //perform a double comparison
                return this.DoubleValue.CompareTo(other.DoubleValue);
            }
            else
            {
                //both double
                return this._doubleValue.CompareTo(other._doubleValue);
            }
        }

        #endregion

        #region Private Methods

        private static bool IsInRangeForLong(double value)
        {
            double maxLongValue = long.MaxValue - 513;
            double minLongValue = long.MinValue + 513;

            return (value < maxLongValue && value > minLongValue);
        }

        #endregion
    }
}
